package com.yeziji.common.base;

import cn.hutool.core.util.StrUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.yeziji.annotation.ShouldEncryption;
import com.yeziji.common.CommonEntity;
import com.yeziji.common.CommonErrorMsg;
import com.yeziji.constant.SecuritySal;
import com.yeziji.constant.VariousStrPool;
import com.yeziji.exception.ApiException;
import com.yeziji.utils.AesUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
 * 监听器的盐值
 *
 * @author hwy
 * @since 2023/09/01 21:24
 **/
@Slf4j
public abstract class ListenerBase<T> {
    /**
     * 基本字段
     */
    protected static final Set<String> BASE_FIELDS = Set.of("id", "createTime", "updateTime", "isDelete");
    /**
     * 获取加密缓存
     */
    private final Cache<Class<?>, Set<String>> shouldEncryptionCache =
            CacheBuilder.newBuilder()
                    .initialCapacity(5)
                    .maximumSize(20)
                    .build();

    /**
     * 判断当前字段是否为父类字段
     *
     * @param column 字段
     * @return {@link Boolean} 是否为父类
     */
    protected boolean isBaseEntityFields(String column) {
        return BASE_FIELDS.contains(column);
    }

    /**
     * 加密字段
     * <p>对被 {@link ShouldEncryption} 注解的字段进行加密</p>
     *
     * @param data 数据本身
     * @param salt 加密盐值
     */
    protected void encryptionColumns(T data, String salt) {
        if (data == null) {
            return;
        }
        this.aesColumns(data, salt);
    }

    /**
     * 加密字段
     * <p>对被 {@link ShouldEncryption} 注解的字段进行加密</p>
     *
     * @param data 数据本身
     */
    protected void encryptionColumns(T data) {
        if (data == null) {
            return;
        }
        this.aesColumns(data, VariousStrPool.EMPTY);
    }

    /**
     * 强制类型
     *
     * @param obj 强转对象
     * @return {@link T} 泛型对象
     */
    @SuppressWarnings("unchecked")
    protected T convertObj(Object obj) {
        if (obj != null) {
            return (T) obj;
        }
        return null;
    }

    /**
     * 解析 value
     *
     * @param clazz     解析的类
     * @param fieldName 字段名称
     * @param value     数值对象
     * @param salt      加密盐值
     * @return {@link Object} 解密后的 value
     */
    protected Object decryptValue(Class<? extends CommonEntity> clazz, String fieldName, Object value, String salt) {
        try {
            if (this.isBaseEntityFields(fieldName) ||
                    !clazz.getDeclaredField(fieldName).isAnnotationPresent(ShouldEncryption.class) ||
                    StringUtils.isBlank(fieldName)) {
                return value;
            }
            // null 就直接返回
            if (value == null) {
                return null;
            }
            // 返回解密字段
            return AesUtils.decrypt(String.valueOf(value), this.getRealSalt(clazz.getDeclaredField(fieldName), salt));
        } catch (NoSuchFieldException e) {
            return value;
        }
    }

    /**
     * aes 操作字段
     *
     * @param data 数据
     * @param salt aes 加密盐值
     */
    private void aesColumns(T data, String salt) {
        if (data == null) {
            return;
        }

        Class<?> entityClass = data.getClass();
        Set<String> fieldNames = Optional.ofNullable(shouldEncryptionCache.getIfPresent(entityClass)).orElseGet(HashSet::new);
        if (CollectionUtils.isEmpty(fieldNames)) {
            Arrays.stream(data.getClass().getDeclaredFields())
                    .peek(field -> field.setAccessible(true))
                    .filter(field -> {
                        try {
                            // 将需要加密的字段保存至 filedName 中
                            boolean isShouldEncryption = field.isAnnotationPresent(ShouldEncryption.class);
                            if (isShouldEncryption) {
                                fieldNames.add(field.getName());
                            }
                            // 有值才能做加解密操作
                            return isShouldEncryption && field.get(data) != null;
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            throw new ApiException(CommonErrorMsg.SYSTEM_THROW_ERROR);
                        }
                    })
                    .forEach(field -> {
                        try {
                            String objStr = String.valueOf(field.get(data));
                            field.set(data, AesUtils.encrypt(objStr, this.getRealSalt(field, salt)));
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            log.error("加密异常 -> {}", e.getMessage(), e);
                            throw new ApiException(CommonErrorMsg.SYSTEM_THROW_ERROR);
                        }
                    });
            // 保存字段
            shouldEncryptionCache.put(entityClass, fieldNames);
        } else {
            for (String fieldName : fieldNames) {
                try {
                    Field field = entityClass.getDeclaredField(fieldName);
                    field.setAccessible(true);
                    Object fieldValue = field.get(data);
                    if (fieldValue != null) {
                        String objStr = String.valueOf(fieldValue);
                        field.set(data, AesUtils.encrypt(objStr, this.getRealSalt(field, salt)));
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    log.error("缓存字段异常 -> {}", e.getMessage(), e);
                    throw new ApiException(CommonErrorMsg.SYSTEM_THROW_ERROR);
                }
            }
        }
    }

    /**
     * 获取实际加密的盐值
     *
     * @param field        加密字段
     * @param originalSalt 入参盐值
     * @return {@link String} 真实的盐值
     */
    private String getRealSalt(Field field, String originalSalt) {
        String realSalt;
        if (StrUtil.isBlank(originalSalt)) {
            ShouldEncryption encryption = field.getAnnotation(ShouldEncryption.class);
            realSalt = StrUtil.blankToDefault(encryption.salt(), SecuritySal.COMMON_SALT);
        } else {
            realSalt = originalSalt;
        }
        return realSalt;
    }
}
